package ggcache

import (
	"sync"
	"time"
)

// MAX_MOD max value of mod. When param passed is bigger than MAX_MOD,it is set ot MAX_MOD
const MAX_MOD uint32 = 64
const MIN_MOD uint32 = 2

// MAX_NUMS_DELETE max value of keys to delete one time.
const MAX_NUMS_DELETE int = 100

type GgCache struct {
	mod        uint32        // mod value
	maps       []*GgCacheMap // maps
	gcInterval time.Duration // interval of time to delete keys.
}

// NewGgCache new gg cache instance
func NewGgCache(options ...Option) *GgCache {
	gg := &GgCache{mod: MIN_MOD, gcInterval: time.Second}

	for _, f := range options {
		f(gg)
	}

	// init map
	gg.maps = make([]*GgCacheMap, int(gg.mod), int(gg.mod))

	for i := 0; i < int(gg.mod); i++ {
		gg.maps[i] = NewGgCacheMap()
	}

	// delete the keys expired background.
	go deleteKeys(gg)

	return gg
}

// Put put or update one key
func (gg *GgCache) Put(key, value interface{}, exp int64) {
	mIndex := crc32_(key) % gg.mod
	gg.maps[(mIndex)].put(key, value, exp)
}

// Get get one key
func (gg *GgCache) Get(key interface{}) (interface{}, bool) {
	mIndex := gg.getIndex(key)
	return gg.maps[(mIndex)].get(key)
}

// GetWithSource when the key does not exist ,call fn to get data,and update the cache
func (gg *GgCache) GetWithSource(key interface{}, fn SourceFunc, exp int64) (interface{}, bool, error) {
	mIndex := gg.getIndex(key)
	if value, ok := gg.maps[(mIndex)].get(key); ok {
		return value, ok, nil
	} else {
		value, exist, err := fn()
		if err != nil {
			return value, exist, err
		}

		// update cache
		gg.maps[mIndex].put(key, value, exp)

		return value, exist, nil
	}
}

// Delete delete one key
func (gg *GgCache) Delete(key interface{}) {
	mIndex := gg.getIndex(key)
	gg.maps[(mIndex)].delete(key)
}

// getIndex get the index of map in the slice.
func (gg *GgCache) getIndex(key interface{}) uint32 {
	return crc32_(key) % gg.mod
}

// GgCacheMeta meta data of cache
type GgCacheMeta struct {
	key   interface{}
	value interface{}
	exp   int64
}

type GgCacheMap struct {
	data sync.Map
}

// NewGgCacheMap new cache map
func NewGgCacheMap() *GgCacheMap {
	return &GgCacheMap{data: sync.Map{}}
}

// put add/update
// exp expire time in seconds
func (gm *GgCacheMap) put(key, value interface{}, exp int64) {
	v := GgCacheMeta{
		key:   key,
		value: value,
		exp:   0,
	}

	if exp > 0 {
		v.exp = time.Now().Unix() + exp
	}

	gm.data.Store(key, v)
}

// get get a value from cache.
func (gm *GgCacheMap) get(key interface{}) (interface{}, bool) {
	v, ok := gm.data.Load(key)

	if !ok {
		return nil, false
	}

	vv := v.(GgCacheMeta)

	if vv.exp > 0 && vv.exp < time.Now().Unix() {
		gm.delete(key)
		return nil, false
	}

	return vv.value, true
}

// delete delete a key. use delete method.
func (gm *GgCacheMap) delete(key interface{}) {
	gm.data.Delete(key)
}

// deleteKeys delete keys batch
func (gm *GgCacheMap) deleteKeys(keys ...interface{}) {
	for _, key := range keys {
		gm.delete(key)
	}
}

// deleteKeys delete expire keys regularly background
func deleteKeys(gg *GgCache) {
	timer := time.NewTicker(gg.gcInterval)

	for {
		<-timer.C

		// execute delete task
		for i := 0; i < len(gg.maps); i++ {
			findAndDeleteKeys(gg.maps[i])
		}
	}
}

// findAndDeleteKeys find expired keys and delete
func findAndDeleteKeys(gm *GgCacheMap) {
	var counter int

	gm.data.Range(func(k, v interface{}) bool {
		vv := v.(GgCacheMeta)
		if vv.exp > 0 && vv.exp < time.Now().Unix() {
			gm.delete(k)
			counter++

			if counter >= MAX_NUMS_DELETE {
				return false
			}
		}

		return true
	})
}

// Deleter return a chan to notify gg to delete key
type Deleter func() <-chan string

// SetDeleter  set a deleter
func SetDeleter(gg *GgCache, fc Deleter) {
	ch := fc()

	go func() {
		for {
			select {
			case v := <-ch:
				gg.Delete(v)
			}
		}
	}()
}
